上一章節,我們介紹了如何使用各種運算子進行邊緣檢測,檢測出來的結果是一張帶有邊緣的影像。然而,我們仍不知道要如何從邊緣影像中取得輪廓中每一個點的座標位置,像是取得感興趣區域(ROI)、凸包、最小外接圓等運算,都需要先知道輪廓中的點。在這一章的內容會教你如何使用OpenCV在邊緣影像中提取輪廓,以及如何取得每個輪廓點的座標位置。
輪廓是由多個點組成的連續曲線或多邊形,這些點會包圍物體或形狀的邊界。輪廓描述了物體在影像中的外形和輪廓的形狀。
看到下圖為一張黑白的影像,我們先將影像中每個輪廓都進行標號,注意如果形狀不是實心的,在形狀的內側也會有輪廓,像是輪廓0還有一個內側的輪廓1,反之輪廓2和輪廓3因為是實心的所以只有一個。
輪廓階層(Contour Hierarchy)用於描述和組織影像中多個輪廓之間的關係。當一張影像中有多個物體或形狀的輪廓存在時,輪廓階層的作用就像是一個家譜,讓我們可以清晰的了解這些輪廓之間的層次關係。
輪廓階層可以被視為一個樹狀結構,其中包含以下三個主要元素:
下圖就是這張圖片完整的輪廓樹狀圖,可以把它想像成輪廓的祖譜,這張圖完美的詮釋了輪廓的層次結構,誰是父親、兒子和兄弟姊妹一目了然。
在OpenCV上,一張圖片具有多個輪廓,這些輪廓的階層會被OpenCV包裝成一個vector<cv::Vec4i>
,可以想像成一個cv::Vec4i的陣列。當然,這樣講不精確,vector<>
其實是C語言向量的表示方式,只不過你也可以把它當成一個可以任意增加、刪除元素的陣列,在開發OpenCV時會經常使用到,概念類似Java的List<T>,或是python的List[T]。
那cv::Vec4i
又是什麼東西?cv::Vec4i
是一個有號整數四維的向量,他不像前面講的vector<>
具有增加、刪除元素的特性。反之,這個向量的元素數量固定是4,而且這個型別是OpenCV特有的向量表示法,可以參與一些矩陣運算。如果你想要了解更多有關Vec4i的用法,可以參考 Basic Structures Vec。
當你透過OpenCV尋找輪廓以後,OpenCV會輸出輪廓階層和輪廓點的向量,輪廓階層會被包裝成Vec4i,這個向量裡的四個元素分別代表不同的資訊。
使用OpenCV樹狀的檢索模式,完整的輪廓階層樹狀圖輸出:
0 ->[6, -1, 1, -1]
1 ->[-1, -1, 2, 0]
2 ->[4, -1, 3, 1]
3 ->[-1, -1, -1, 2]
4 ->[-1, 2, 5, 1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 0, -1, -1]
輪廓檢索模式主要影響輪廓檢測後,如何組織和儲存檢測到的輪廓。
最常見的輪廓檢索模式,這個模式會檢索最外層的輪廓,忽略所有內部輪廓。換句話說,它只檢索影像中最外層的物體邊界,其他子類通通忽略掉,經常被應用在抓出感興趣區域(ROI)的範圍。
使用OpenCV的向量表示為:
0 ->[6, -1, -1, -1]
6 ->[-1, 0, -1, -1]
這種模式檢索所有的輪廓,模式會檢索所有輪廓並將它們存儲在列表中,但這種檢索方法無法表示輪廓的層次結構,因為所有檢測到的輪廓均為同層輪廓。
使用OpenCV的向量表示為:
0 ->[1, -1, -1, -1]
1 ->[2, 0, -1, -1]
2 ->[3, 1, -1, -1]
3 ->[4, 2, -1, -1]
4 ->[5, 3, -1, -1]
5 ->[6, 4, -1, -1]
6 ->[-1, 5, -1, -1]
這種模式檢索所有輪廓,但將輪廓會被分為兩個層次,最外層的輪廓位於第一層,而內部的輪廓位於第二層。最上層的輪廓階層最多只能包含一個子類,且子類不會再有任何子類,所以要使用這個方法完整描述輪廓的層次還是有困難,但足以應付一些使用情境。
使用OpenCV的向量表示為:
0 ->[2, -1, 1, -1]
1 ->[-1, -1, -1, 0]
2 ->[4, 0, 3, -1]
3 ->[-1, -1, -1, 2]
4 ->[6, 2, 5, -1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 4, -1, -1]
這種模式檢索所有的輪廓,並將它們組織成一個層次樹狀結構。每個輪廓都有一個父輪廓和零個或多個子輪廓,完美描述了輪廓的階層關係。
使用OpenCV的向量表示為:
0 ->[6, -1, 1, -1]
1 ->[-1, -1, 2, 0]
2 ->[4, -1, 3, 1]
3 ->[-1, -1, -1, 2]
4 ->[-1, 2, 5, 1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 0, -1, -1]
下面的程式碼展示了如何使用RETR_TREE檢索模式 尋找輪廓,執行的流程如下:
cv::RETR_TREE
作為輪廓檢索模式,以及cv::CHAIN_APPROX_TC89_KCOS
作為輪廓近似方法。使用OpenCV的cv::findContours
函數,該函數用於在二值化影像中尋找輪廓。採坑注意,傳入的圖片不需先做邊緣檢測(Canny、Sobel...等),否則檢測出來的輪廓數量會變成兩倍。
cv::findContours(binary_img,contours,hierarchy,cv::RETR_TREE,cv::CHAIN_APPROX_TC89_KCOS);
binary_img
:輸入的二值化影像。contours
:一個儲存檢測到的輪廓的向量,以vector<vector<cv::Point>>
的形式存儲在這個向量中。hierarchy
:一個存儲輪廓的階層結構的向量,以vector<cv::Vec4i>
的形式存儲在這個向量中。每個cv::Vec4i
表示一個輪廓的層次關係,包括父輪廓索引、子輪廓索引、同層輪廓索引。cv::RETR_TREE
:輪廓檢索模式。在這裡被設置為cv::RETR_TREE
,檢索所有輪廓。cv::CHAIN_APPROX_TC89_KCOS
:這是輪廓近似方法的一個選項。設置為cv::CHAIN_APPROX_TC89_KCOS
。這行程式碼使用了OpenCV的cv::boundingRect
函數,用於計算輪廓的外接矩形。以下是這行程式碼的解釋:
cv::Rect rect=cv::boundingRect(contours.at(i));
contours.at(i)
:從contours
向量中獲取第i
個輪廓。使用OpenCV的cv::drawContours
函數來在 output_img
影像上繪製輪廓。
cv::drawContours(output_img, contours,i, cv::Scalar(0,0,255), 1,cv::LINE_8);
output_img
:這是目標影像,也就是要在其上繪製輪廓的影像。contours
: 包含多個輪廓,每個輪廓都是一個 vector<cv::Point>
。i
:這是要繪製的輪廓的索引。在這行程式碼中,它被設置為 i
,這表示我們將繪製 contours
中的第 i
個輪廓。cv::Scalar(0,0,255)
:這個參數指定了繪製輪廓的顏色,這裡使用(0,0,255)
純紅色。cv::LINE_8
:繪製輪廓線的類型。這行程式碼是用來處理輪廓階層(Hierarchy)中的一部分信息,主要用於分析和顯示輪廓之間的父子關係。以下是這段程式碼的解釋:
cv::transpose
函數將vec
中的元素轉置到v
中,這樣一來Vec4i的4x1的向量會被轉置成1x4的矩陣。cv::Vec4i vec=hierarchy[i];
cv::Mat v;
cv::transpose(vec, v);
printf("%d ->",i);
print(v);
printf(" points:%d\n",(int)contours[i].size());
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include "opencv2/core/utils/logger.hpp"
using namespace std;
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
cv::Mat grayImage = cv::imread("C:\\Users\\vince\\Downloads\\test_image3.jpg",cv::IMREAD_GRAYSCALE);
cv::namedWindow("Output", cv::WINDOW_NORMAL);
cv::resizeWindow("Output", cv::Size(512*((float)grayImage.cols)/grayImage.rows,512));
cv::Mat binary_img;
cv::threshold(grayImage, binary_img, 0, 255, cv::THRESH_OTSU);
vector<vector<cv::Point>> contours;
vector<cv::Vec4i> hierarchy;
cv::findContours(binary_img,contours,hierarchy,cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE);
printf("counters found:%d\n",(int)contours.size());
printf("\n");
cv::Mat output_img=cv::Mat::zeros(cv::Size(binary_img.cols,binary_img.rows),CV_8UC3);
for (int i = 0;i < hierarchy.size();i++) {
cv::Vec4i vec=hierarchy[i];
cv::Mat v;
cv::transpose(vec, v);
printf("%d ->",i);
print(v);
printf(" points:%d\n",(int)contours[i].size());
cv::Rect rect=cv::boundingRect(contours.at(i));
cv::rectangle(output_img, rect, cv::Scalar(0,255,0),1);
cv::putText(output_img, std::to_string(i),cv::Point(rect.x, rect.y-10),cv::FONT_HERSHEY_COMPLEX,0.4,cv::Scalar(0, 255,0),1);
cv::drawContours(output_img, contours,i, cv::Scalar(0,0,255), 1,cv::LINE_8);
}
cv::imshow("Output",output_img);
cv::imwrite("output.jpg", output_img);
cv::waitKey(0);
return 0;
}
counters found:2
0 ->[1, -1, -1, -1] points:22
1 ->[-1, 0, -1, -1] points:4
counters found:7
0 ->[1, -1, -1, -1] points:8
1 ->[2, 0, -1, -1] points:4
2 ->[3, 1, -1, -1] points:8
3 ->[4, 2, -1, -1] points:22
4 ->[5, 3, -1, -1] points:8
5 ->[6, 4, -1, -1] points:22
6 ->[-1, 5, -1, -1] points:4
counters found:7
0 ->[2, -1, 1, -1] points:4
1 ->[-1, -1, -1, 0] points:8
2 ->[4, 0, 3, -1] points:22
3 ->[-1, -1, -1, 2] points:8
4 ->[6, 2, 5, -1] points:22
5 ->[-1, -1, -1, 4] points:8
6 ->[-1, 4, -1, -1] points:4
counters found:7
0 ->[6, -1, 1, -1] points:22
1 ->[-1, -1, 2, 0] points:8
2 ->[4, -1, 3, 1] points:4
3 ->[-1, -1, -1, 2] points:8
4 ->[-1, 2, 5, 1] points:22
5 ->[-1, -1, -1, 4] points:8
6 ->[-1, 0, -1, -1] points:4